package com.hmdp.service.impl;


import java.time.LocalDateTime;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.common.Code;
import com.hmdp.common.Constants;
import com.hmdp.common.R;
import com.hmdp.common.RedisData;
import com.hmdp.entity.Shop;
import com.hmdp.exception.BusinessException;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.hmdp.utils.RedisKeyUtils;
import com.hmdp.utils.RedisUtil;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 公众号：是叶十三
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService, Constants {

    public static final int FIVE = 5;

    private final StringRedisTemplate redisTemplate;
    private final ShopMapper shopMapper;
    private final RedisUtil redisUtil;


    /**
     * JDK自定义线程池 -缓存重建
     */
    private final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            1,
            1,
            1,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(10)
            );

    @Autowired
    public ShopServiceImpl(StringRedisTemplate redisTemplate, ShopMapper shopMapper, RedisUtil redisUtil ) {
        this.redisTemplate = redisTemplate;
        this.shopMapper = shopMapper;
        this.redisUtil = redisUtil;
    }

    /**
     * v1.0 没有解决缓存穿透问题
     *
     * @param id
     * @return
     */
    @Override
    public R selectByIdV1(Long id) {
        // 1、先从redis查询商铺列表
        String redisKey = RedisKeyUtils.getShopInfoKey(id);
        String jsonShop = redisTemplate.opsForValue().get(redisKey);
        Shop shop = JSON.parseObject(jsonShop, Shop.class);
        // 2、如果redis中有，直接返回
        if (ObjectUtil.isNotNull(shop)) {
            return R.ok(shop);
        }
        // 如果是空字符串，则直接
        // 3、如果redis中没有，去数据库查询
        shop = shopMapper.selectById(id);
        // 4、如果数据库中没有，直接抛出异常
        if (ObjectUtil.isNull(shop)) {
            throw new BusinessException(Code.BUSINESS_ERR, "该数据不存在");
        }
        // 5、如果数据库中有，则备份一份到redis中，返回最终的数据
        redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return R.ok(shop);
    }

    /**
     * v2.0 缓存空对象 -解决缓存穿透问题
     *
     * @param id
     * @return
     */
    @Override
    public R selectByIdV2(Long id) {
        // 1、先从redis查询商铺列表
        String redisKey = RedisKeyUtils.getShopInfoKey(id);
        String jsonShop = redisTemplate.opsForValue().get(redisKey);
        // 2、如果redis中有，直接返回
        Shop shop = null;
        if (StrUtil.isNotBlank(jsonShop)) {
            shop = JSONUtil.toBean(jsonShop, Shop.class);
            return R.ok(shop);
        }
        // 如果是空字符串，则直接返回
        if (jsonShop != null) {
            throw new BusinessException(Code.BUSINESS_ERR, "店铺信息不存在 ！");
        }
        // 3、如果redis中没有，去数据库查询
        shop = shopMapper.selectById(id);
        // 4、如果数据库中没有，直接抛出异常
        if (ObjectUtil.isNull(shop)) {
            // 缓存空字符串， 设置有效期为5分钟
            redisTemplate.opsForValue().set(redisKey, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            throw new BusinessException(Code.BUSINESS_ERR, "该数据不存在");
        }
        // 5、如果数据库中有，则备份一份到redis中，返回最终的数据
        // 将对象转成json
        String jsonShopNew = JSONUtil.toJsonStr(shop);
        redisTemplate.opsForValue().set(redisKey, jsonShopNew, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return R.ok(shop);
    }

    /**
     * v3.0
     * 1、缓存空对象 -解决缓存穿透问题
     * 2、互斥锁 - 解决缓存击穿问题
     *
     * @param id
     * @return
     */
    @Override
    public R selectByIdV3(Long id) {
        // 1、先从redis查询商铺列表
        String redisKey = RedisKeyUtils.getShopInfoKey(id);
        String jsonShop = redisTemplate.opsForValue().get(redisKey);
        // 2、如果redis中有，直接返回
        Shop shop = null;
        if (StrUtil.isNotBlank(jsonShop)) {
            shop = JSONUtil.toBean(jsonShop, Shop.class);
            return R.ok(shop);
        }
        // 如果是空字符串，则直接返回
        if (jsonShop != null) {
            return R.ok(shop);
        }
        // ******加锁*******
        // 构建key
        String lockKey = RedisKeyUtils.getShopLockKey(id);
        try {
            // v4. 获取互斥锁
            boolean flag = redisUtil.tryLock(lockKey, "shop_lock");
            // 4.1 获取锁失败，重试
            if (!flag) {
                Thread.sleep(50);
                return selectByIdV3(id);
            }
            // 4.2 获取锁成功，则可以操作数据库
            // 3、如果redis中没有，去数据库查询
            shop = shopMapper.selectById(id);

            // 4、如果数据库中没有，直接抛出异常
            if (ObjectUtil.isNull(shop)) {
                // 缓存空字符串， 设置有效期为5分钟
                redisTemplate.opsForValue().set(redisKey, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                throw new BusinessException(Code.BUSINESS_ERR, "该数据不存在");
            }
            // 5、如果数据库中有，则备份一份到redis中，返回最终的数据
            // 将对象转成json
            String jsonShopNew = JSONUtil.toJsonStr(shop);
            redisTemplate.opsForValue().set(redisKey, jsonShopNew, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (Exception e) {
            throw new BusinessException(Code.BUSINESS_ERR, "操作过于频繁！");
        } finally {
            redisUtil.unlock(lockKey);
        }
        return R.ok(shop);
    }


    @Override
    public R selectByIdV4(Long id) {
        // 1、先从redis查询商铺列表
        String redisKey = RedisKeyUtils.getShopInfoKey(id);
        RedisData redisData = (RedisData) redisUtil.get(redisKey);
        // 2、如果redis没有，直接返回null
        if (ObjectUtil.isNull(redisData)) {
            return R.fail(null);
        }
        // 4.命中，需要先把json反序列化为对象
        // 获取到数据对象
        Shop data = (Shop) redisData.getData();
        // 获取到逻辑过期时间
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5. 判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 没有过期，直接返回商品信息
            return R.ok(data);
        }
        // 6、已经过期了，需要重建缓存
        // 6.2 获取互斥锁
        String lockKey = RedisKeyUtils.getShopLockKey(id);
        boolean lock = redisUtil.tryLock(lockKey, "shop_lock");
        // 6.3 判断是否获取锁成功
        if (lock) {
            // 开启独立线程，进行缓存重建
            reBuilderCache(id, lockKey);
        }
        // 返回商品信息
        return R.ok(data);
    }

    private void reBuilderCache(Long id, String lockKey) {
        //成功，重建缓存（开辟一个新的线程，去重建缓存）
        threadPoolExecutor.submit(() -> {
            try {
                this.saveShop2Redis(id, 20L);
            } catch (Exception e) {
                throw new BusinessException(Code.SYSTEM_ERR, "系统异常，请稍微后重试！");
            } finally {// 释放锁
                redisUtil.unlock(lockKey);
            }
        });
    }

    /**
     * 缓存预热
     *
     * @param id
     * @param expireTime 逻辑过期时间 - 秒
     */
    public void saveShop2Redis(Long id, Long expireTime) throws InterruptedException {
        // 1、查询店铺数据
        Shop shop = shopMapper.selectById(id);
        Thread.sleep(200);
        // 2、封装逻辑过期时间
        RedisData redisData = new RedisData()
                .setData(shop)
                .setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
        // 3、写入redis
        String redisKey = RedisKeyUtils.getShopInfoKey(id);
        redisUtil.set(redisKey, redisData);
    }
}
